lockfile은 왜 자꾸 귀찮게할까

Lockfile은 왜 필요한걸까

lock 파일은 패키지 설치의 “결과물”을 고정하기 위해 생겼다.

원래 package.json에는 "react": "^18.2.0"처럼 버전 범위가 들어간다. 이걸로 npm install을 하면 매번 최신 18.x 버전을 가져오는데, 이 “최신”이라는 게 팀원마다, 혹은 날짜마다 달라질 수 있다. 즉, 같은 package.json인데도 어제 설치한 사람과 오늘 설치한 사람의 버전 트리가 달라질 수 있다.

이 문제를 해결하기 위해 lockfile이 도입되었다.
lock 파일은 “설치된 당시의 패키지들의 정확한 버전과 의존 트리”를 기록해둔다. 다시 설치할 때는 이 파일을 읽어서 같은 버전을 정확히 재현하기 때문에 “팀원이 달라도, CI 서버라도, 언제 깔아도 똑같은 환경”이 만들어진다.

만약 lock 파일이 없다면, 매번 npm이 네트워크를 타고 새 버전을 받아오면서 의존성 충돌이 발생할 수 있다. 버그 수정이 포함된 새 버전이 자동으로 설치되기도 하지만, 반대로 깨진 버전이 설치될 위험도 생긴다.

왜 lockfile이 마음대로 변할까?

다 개발자 잘못이다. ㅋㅋ

원인 1. lockfile 커밋 누락 문제

lock 파일은 결국 “버전이 고정된 결과물”인데, 실수로 package.json만 수정하고 lock 파일을 안 맞춰놓은 채로 커밋할 때가 있다. 이 상태로 install을 하면 npm이나 yarn이 lock 파일을 업데이트해버린다. 그럼 팀마다 lock 파일이 미묘하게 달라져서 의존성이 다르게 설치되는 경우가 있다.

이 문제가, 빌드 환경에서 발생하면 디버깅을 복잡하게 만든다.
”로컬에선 되는데 서버에선 안됩니다!” 같은..
커밋 잘하자..

원인 2. 패키지 매니저의 버전 차이

패키지 매니저 버전이 다르면 lockfile이 변할 수 있다.
보통 패키지 매니저는 메이저 버전이 바뀔 때 lockfile 포맷을 변경한다.

예를 들어 pnpm 8pnpm 9은 lock 파일의 구조와 생성 규칙이 다르다.

  • pnpm 8에서는 패키지별로 dependenciesMeta 필드가 없었는데
    pnpm 9에서는 그게 생기거나 필드 이름이 바뀜
  • peerDependencies 을 처리하는 로직이 예전에는 느슨했는데,
    새 버전에서는 더 엄격하게 처리해서 lockfile에 명시적으로 추가될 수도 있다.

이 경우, 설치를 실행하는 순간 새 포맷으로 “마이그레이션”이 일어난다.
즉, lock 파일의 구조가 패키지 매니저 버전에 따라 자동 변환된다.
그래서 lock 파일 diff가 생긴다.

원인 3. Node 버전 차이

패키지매니저 버전이 같더라도, Node 버전의 차이가 있으면, lockfile이 변할 수 있다.
물론, Node 21에는 pnpm@8을 설치하고, Node 23에는 pnpm@9를 설치해둔 후에
프로젝트마다 Node 버전을 맞게 바꾸지 않아서, 패키지 매니저 버전이 달라진 이유도 있을 수 있다.
하지만 pnpm버전이 같더라도 Node버전이 다르면 lockfile이 변결 될 수 있다. 예를들어,

  • Node 16에서는 optionalDependencies 중 일부가 설치 대상이 아니고,
  • Node 20에서는 그 패키지가 설치 대상이 되기도 한다.

이런 차이로 인해 설치 트리가 달라지고, 패키지 매니저는 그걸 반영해 lock 파일을 수정한다.

그렇다면..! 어떻게 개선할 수 있을까?

배포환경에서는 --frozen-lockfile 또는 --immutable 을 이용해 의존성을 설치하자
이렇게 하면 커밋 누락 문제를 어느정도 방어할 수 있으며, 오늘 빌드와 내일 빌드가 달라지는 문제를 막을 수 있다.

패키지 매니저와 Node 버전을 팀 전체에서 통일시키는 것도 고려해야 한다.
방법은 여러가지가 있다:

  1. only-allow 라이브러리를 사용하기
  2. Corepack 사용하기
  3. package.json의 engines + .npmrc의 engine-strict=true 활용하기

나는 개인적으로 3번의 방식을 좋아한다.

1. only-allow

only-allow는 프로젝트에서 허용할 패키지 매니저의 종류만 제한할 수 있는 간단한 라이브러리다.
예를 들어 “이 프로젝트는 pnpm만 허용”하고 싶을 때 이렇게 쓴다:

"scripts": {
  "preinstall": "npx only-allow pnpm"
}

이렇게 하면 npm install이나 yarn install을 시도할 때 다음과 같은 에러가 발생한다:

Error: You must use pnpm to install dependencies for this project.

대신 모두가 한 번씩 npm install -g only-allow로 전역 설치를 해줘야하며,
preinstall 스크립트를 꼭 한 번은 실행해야 한다는 단점이 있다.

큰 단점은 버전은 통제할 수 없다는 점이다. 즉, pnpm 8을 쓰든 9를 쓰든 설치는 가능하다.
그래서 팀 정책 수준의 “패키지 매니저 종류 통일” 정도로 쓰인다.


2. Corepack

Node가 직접 패키지 매니저의 종류와 버전을 자동으로 통제한다.

# package.json
'packageManager': 'pnpm@9.11.0'
corepack enable
corepack prepare pnpm@9.11.0 --activate

이렇게 설정하면,
pnpm install을 실행할 때 Node가 package.json을 읽고 "packageManager": "pnpm@9.11.0" 정보를 확인한다. 그 버전이 설치되어 있지 않으면 자동으로 내려받고, 해당 버전으로 설치를 실행한다. 즉, Node 버전이 달라도, CI든 로컬이든 항상 동일한 pnpm 버전으로 install이 수행된다.

각 개발자와 CI 환경에서 corepack enable을 한 번씩 실행해줘야 강제할 수 있다는 단점이 있다.


3. engines, engine-strict

개인적으로 선호하는 방식이다.

이 방식은 corepack처럼 자동으로 인식해주진 않지만, 설정만 해두면 Node와 패키지 매니저 버전을 동시에 강제할 수 있다. 누군가 설정만 해둔다면, 팀 전체에서 동일한 Node와 패키지 매니저 버전을 강제할 수 있다는 장점이 있다.

# package.json
"engines": {
  "node": "=20.12.2",
  "pnpm": "=10.8.1",
  "npm" : "please use pnpm",
  "yarn" : "please use pnpm",
},

# .npmrc
engine-strict=true

pnpm 버전이나 node버전이 engines와 맞지 않을 때는 에러를 뱉으며 설치를 중단한다.
의존성 추가에도 에러를 뱉기 때문에, 초장에 문제를 잡을 수 있다.

# PNPM 버전이 engines의 명시한 것과 일치하지 않을 때
Your pnpm version is incompatible with "/Users/.....".

Expected version: =10.8.1
Got: 8.15.6

This is happening because the package's manifest has an engines.pnpm field specified.
To fix this issue, install the required pnpm version globally.

To install the latest version of pnpm, run "pnpm i -g pnpm".
To check your pnpm version, run "pnpm -v".
# Node 버전이 engines의 명시한 것과 일치하지 않을 때
Your Node version is incompatible with "/Users/.....".

Expected version: =20.12.2
Got: v21.6.1

This is happening because the package's manifest has an engines.node field specified.
To fix this issue, install the required Node version.
# yarn으로 설치 시도할 때
error interview@0.1.0: The engine "yarn" is incompatible with this module. Expected version "please use pnpm". Got "1.22.19"
error Found incompatible module.
# npm으로 설치 시도할 때
npm ERR! engine Unsupported engine
npm ERR! engine Not compatible with your version of node/npm: interview@0.1.0
npm ERR! notsup Required: {"node":"=20.12.2","pnpm":"=10.8.1","npm":"please use pnpm","yarn":"please use pnpm"}
npm ERR! notsup Actual:   {"npm":"10.5.0","node":"v20.12.2"}

마지막으로,
nvmrc파일까지 추가해서 nvm use 명령어로 쉽게 node 버전을 맞춰주는 것 까지 해준다면 아주 좋다.

# .nvmrc
20.12.2